

const TYPE_DATAURL = 'dataurl'
const TYPE_BLOB = 'blob'

const base64RegExp = /^data:(image\/\S+);base64,/

let createClass = config=>{
    let { defaultMIMEType } = config || {}

    return class Compressor {
        constructor(img){
            // dataURL / image
            if(typeof img === 'string' && base64RegExp.test(img)){
                // base64URL
                this.type = TYPE_DATAURL
                this.mimeType = base64RegExp.exec(img)?.[1]
            }else if(img instanceof Blob){
                this.type = TYPE_BLOB
                this.mimeType = img.type
            }else{
                console.warn(`the type of parameter is invalid!: (typeof)${typeof img} (instanceof Blob)${img instanceof Blob}`)
            }
            this.data = img
        }

        static create(c){
            return createClass(c)
        }

        static async toImageLoaded(image){
            return new Promise((r, reject)=>{
                if(typeof image === 'string'){
                    let src = image
                    image = new Image()
                    image.onload = ()=>r(image)
                    image.onerror = reject
                    image.src = src
                }else if(image instanceof Blob){
                    let src = URL.createObjectURL(image)
                    image = new Image()
                    image.onload = ()=>r(image)
                    image.onerror = reject
                    image.src = src
                }else if(image instanceof HTMLImageElement || image instanceof Image){
                    if(!image.complete){
                        image.onload = ()=>r(image)
                        image.error = reject
                    }else{
                        r(image)
                    }
                }else{
                    reject(`toImageLoaded参数格式错误: ${image}`)
                }
            })
        }

        static async toCanvasByImage(image, {
            x = 0,
            y = 0,
            width,
            height,
            sx = 0,
            sy = 0,
            sWidth,
            sHeight,
        } = {} /* rect*/){
            sWidth = sWidth || image.width
            sHeight = sHeight || image.height
            width = width || sWidth
            height = height || sHeight
            return Compressor.toImageLoaded(image).then(image=>{
                let canvas = document.createElement('canvas')
                canvas.width = sWidth
                canvas.height = sHeight
                let ctx = canvas.getContext('2d')
                ctx.drawImage(image, sx, sy, sWidth, sHeight, x, y, width, height)
                return canvas
            })
        }

        static async canvasToBlob(canvas, toMIMEType, quality){
            return new Promise((r)=>{
                canvas.toBlob(r, toMIMEType || defaultMIMEType, quality)
            })
        }

        static async fromImage(image){
            if(typeof image === 'string' && base64RegExp.test(image) || image instanceof Blob){
                return new Compressor(image)
            }else{
                return Compressor.toImageLoaded(image).then(image=>{
                    return Compressor.toCanvasByImage(image).then(canvas=>{
                        return Compressor.canvasToBlob(canvas)
                    }).then(blob=>{
                        return new Compressor(blob)
                    })
                })
            }
        }

        static _isBlob(type){
            return type === TYPE_BLOB
        }
        _isBlob(type){
            return Compressor._isBlob(type || this.type)
        }
        static _isDataURL(type){
            return type === TYPE_DATAURL
        }
        _isDataURL(type){
            return Compressor._isDataURL(type || this.type)
        }

        static getSize(data){
            return data?.size || data?.length || 0
        }

        getSize(data){
            data = data || this.data
            return Compressor.getSize(data)
            // if(this._isDataURL()){
            //     return data?.length || 0
            // }else if(this._isBlob()){
            //     return data?.size || 0
            // }
        }

        _getMIMEType(){
            return this.mimeType || defaultMIMEType
        }

        // toType:  TYPE_BLOB
        //          TYPE_DATAURL
        // rest: mimeType, quality
        static async reform({
            data,
            toType,
            mimeType: toMIMEType,
            quality,
        }){
            toMIMEType = toMIMEType || this._getMIMEType()
            return Compressor.toImageLoaded(data).then(image=>{
                return Compressor.toCanvasByImage(image).then(canvas=>{
                    if(Compressor._isBlob(toType)){
                        return new Promise(r=>{
                            canvas.toBlob(r, toMIMEType, quality)
                        })
                    }else if(Compressor._isDataURL(toType)){
                        return canvas.toDataURL(toMIMEType, quality)
                    }
                })
            })
        }

        async reform(...rest){
            return Compressor.reform({
                data: this.data,
                ...(rest?.[0] || {}),
            }).then(res=>{
                this.data = res
                return res
            })
        }

        reformToDataURL(mimeType, quality){
            return this.reform({
                toType: TYPE_DATAURL,
                mimeType,
                quality})
        }
        reformToBlob(mimeType, quality){
            return this.reform({
                toType: TYPE_BLOB,
                mimeType,
                quality})
        }
        async toDataURL(){
            if(this._isBlob()){
                return new Promise((r, reject)=>{
                    let reader = new FileReader()
                    reader.onload = e=>r(e.target.result)
                    reader.onerror = reject
                    reader.readAsDataURL(this.data)
                })
            }else if(this._isDataURL()){
                return this.data
            }
        }
        toBlob(){
            if(this._isBlob()){
                return this.data
            }else if(this._isDataURL()){
                var arr = this.data?.split(','), mime = arr[0].match(/:(.*?);/)[1],
                bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n)
                while(n--){
                    u8arr[n] = bstr.charCodeAt(n)
                }
                return new Blob([u8arr], {type:mime})
            }else{
                return new Blob
            }
        }

        async compressToBlob(configs){
            return this.compress({
                toType: TYPE_BLOB,
                ...configs
            })
        }

        async compressToDataURL(configs){
            return this.compress({
                toType: TYPE_DATAURL,
                ...configs
            })
        }

        async compress({
            toSize,
            threshold = .1,
            toType,
        }){
            toType = toType || this.type
            let data = this.data
            // if(this.mimeType !== 'image/jpeg'){
                console.warn(`参数MIME(current: ${this.mimeType})， 将会转换成image/jpeg`)
                data = await Compressor.reform({
                    data,
                    mimeType: 'image/jpeg',
                    toType,
                    quality: 1,
                })
            // }
            let initialSize = Compressor.getSize(data)
            console.debug(`图片大小(current: ${initialSize}, to: ${toSize})`)
            if(initialSize < toSize){
                return data
            }else{
                let _compress = ({
                    // data,
                    toSize,
                    threshold,
                    toType,

                    _quality = 50,
                    _prevQuality = 100,
                    _maxTry = 20,
                    _count = 0,
                } = {})=>{
                    if(_count > _maxTry){
                        console.warn(`大于最大尝试次数(_maxTry: ${_maxTry})，停止压缩(current: ${Compressor.getSize(data)})`)
                        return Compressor.reform({
                            data: this.data,
                            quality: _prevQuality / 100,
                            mimeType: 'image/jpeg',
                            toType,
                        })
                    }
                    _count += 1
                    return Compressor.reform({
                        data: this.data,
                        quality: _quality / 100,
                        mimeType: 'image/jpeg',
                        toType,
                    }).then(data=>{
                        let size = Compressor.getSize(data)
                        let reasonableSize = threshold < 1 ? threshold * toSize : threshold
                        let lSize = toSize - reasonableSize
                        let rSize = toSize
                        let nextQualityOffset = Math.abs(_quality - _prevQuality) / 2
                        // if(Math.abs(size - toSize) < reasonableSize){
                        if(size >= lSize && size < rSize){

                            console.debug(`match: quality(${_quality}) currentSize(${size / 1024}KBs) abs(${Math.abs(size - toSize)}Bytes) reasonableSize(${reasonableSize}Bytes) toSize(${toSize / 1024}KBs)`)
                            return data
                        }
                        let toQuality = _quality
                        if(size >= rSize){
                            if(nextQualityOffset < 1){
                                nextQualityOffset = 1
                            }
                            toQuality -= Math.floor(nextQualityOffset)
                            console.debug(`to(-): quality(${toQuality}) currentSize(${size / 1024}KBs) toSize(${toSize / 1024}Kbs`)
                        }else{
                            if(nextQualityOffset < 1){
                                nextQualityOffset = 0
                            }
                            toQuality += Math.floor(nextQualityOffset)
                            console.debug(`to(+): quality(${toQuality}) currentSize(${size / 1024}KBs) toSize(${toSize / 1024}Kbs)`)
                        }
                        if(_quality === toQuality){
                            console.debug(`quality精度已到达1(currentSize): ${size / 1024}Kbs toSize(${toSize}Bytes)`)
                            return data
                        }
                        if(toQuality < 0){
                            console.debug(`该分辨率下quality已经最小，无法继续压缩: ${size / 1024}Kbs toSize(${toSize}Bytes)`)
                            return data
                        }
                        return _compress({
                            // data,
                            toSize,
                            threshold,
                            toType,

                            _quality: toQuality,
                            _prevQuality: _quality,
                            _maxTry,
                            _count,
                        })
                    })
                }
                return _compress({
                    // data,
                    toSize,
                    threshold,
                    toType,
                })
            }
        }
    }
}

export default createClass()